CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/pages/news/[id].tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2023 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Alert, Breadcrumb, Col, Layout, Radio, Row } from "antd";
7
import { GetServerSidePropsContext } from "next";
8
import { useRouter } from "next/router";
9
import NextHead from "next/head";
10
import dayjs from "dayjs";
11
12
import { getNewsItemUserPrevNext } from "@cocalc/database/postgres/news";
13
import getCustomize from "@cocalc/database/settings/customize";
14
import { Icon } from "@cocalc/frontend/components/icon";
15
import { markdown_to_cheerio } from "@cocalc/frontend/markdown";
16
import { slugURL } from "@cocalc/util/news";
17
import { NewsPrevNext } from "@cocalc/util/types/news";
18
19
import Footer from "components/landing/footer";
20
import Head from "components/landing/head";
21
import Header from "components/landing/header";
22
import A from "components/misc/A";
23
import { News } from "components/news/news";
24
import { NewsWithFuture } from "components/news/types";
25
import { useDateStr } from "components/news/useDateStr";
26
import Loading from "components/share/loading";
27
import { MAX_WIDTH, NOT_FOUND } from "lib/config";
28
import { Customize, CustomizeType } from "lib/customize";
29
import useProfile from "lib/hooks/profile";
30
import { extractID } from "lib/news";
31
import withCustomize from "lib/with-customize";
32
33
interface Props {
34
customize: CustomizeType;
35
news: NewsWithFuture;
36
prev?: NewsPrevNext;
37
next?: NewsPrevNext;
38
metadata: {
39
title: string;
40
author: string;
41
url: string;
42
image: string;
43
published: string;
44
modified: string;
45
}
46
}
47
48
const formatNewsTime = (newsDate: NewsWithFuture['date']) => (
49
typeof newsDate === "number" ? dayjs.unix(newsDate) : dayjs(newsDate)
50
).toISOString();
51
52
export default function NewsPage(props: Props) {
53
const { customize, news, prev, next, metadata } = props;
54
const { siteName } = customize;
55
const router = useRouter();
56
const profile = useProfile({ noCache: true });
57
const isAdmin = profile?.is_admin;
58
const dateStr = useDateStr(news);
59
const permalink = slugURL(news);
60
61
const title = `${news.title} – News – ${siteName}`;
62
63
function future() {
64
if (news.future && !isAdmin) {
65
return (
66
<Alert type="info" banner={true} message="News not yet published" />
67
);
68
}
69
}
70
71
function content() {
72
if (profile == null) return <Loading />;
73
if (!isAdmin && news.hide) {
74
return <Alert type="error" message="Not authorized" />;
75
}
76
if (isAdmin || !news.future) {
77
return <News news={news} showEdit={isAdmin} standalone />;
78
}
79
}
80
81
function breadcrumb() {
82
const items = [
83
{ key: "/", title: <A href="/">{siteName}</A> },
84
{ key: "/news", title: <A href="/news">News</A> },
85
{
86
key: "permalink",
87
title: (
88
<A href={permalink}>
89
{isAdmin || (!news.future && !news.hide) ? (
90
<>
91
{dateStr}: {news.title}
92
</>
93
) : (
94
"Not Authorized"
95
)}
96
</A>
97
),
98
},
99
];
100
return <Breadcrumb items={items} />;
101
}
102
103
function olderNewer() {
104
return (
105
<Radio.Group buttonStyle="outline" size="small">
106
<Radio.Button
107
disabled={!prev}
108
style={{ userSelect: "none" }}
109
onClick={() => {
110
prev && router.push(slugURL(prev));
111
}}
112
>
113
<Icon name="arrow-left" /> Older
114
</Radio.Button>
115
<Radio.Button
116
style={{ userSelect: "none" }}
117
onClick={() => {
118
router.push("/news");
119
}}
120
>
121
<Icon name="arrow-up" /> Overview
122
</Radio.Button>
123
<Radio.Button
124
disabled={!next}
125
style={{ userSelect: "none" }}
126
onClick={() => {
127
next && router.push(slugURL(next));
128
}}
129
>
130
<Icon name="arrow-right" /> Newer
131
</Radio.Button>
132
</Radio.Group>
133
);
134
}
135
136
function renderTop() {
137
return (
138
<Row justify="space-between" gutter={15} style={{ margin: "30px 0" }}>
139
<Col>{breadcrumb()}</Col>
140
<Col>{olderNewer()}</Col>
141
</Row>
142
);
143
}
144
145
return (
146
<Customize value={customize}>
147
<Head title={title} />
148
<NextHead>
149
<meta property="og:type" content="article"/>
150
151
<meta property="og:title" content={metadata.title}/>
152
<meta property="og:url" content={metadata.url}/>
153
<meta property="og:image" content={metadata.image}/>
154
155
<meta property="article:published_time" content={metadata.published}/>
156
<meta property="article:modified_time" content={metadata.modified}/>
157
</NextHead>
158
<Layout>
159
<Header/>
160
<Layout.Content
161
style={{
162
backgroundColor: "white",
163
}}
164
>
165
<div
166
style={{
167
minHeight: "75vh",
168
maxWidth: MAX_WIDTH,
169
padding: "30px 15px",
170
margin: "0 auto",
171
}}
172
>
173
{renderTop()}
174
{future()}
175
{content()}
176
</div>
177
<Footer />
178
</Layout.Content>
179
</Layout>
180
</Customize>
181
);
182
}
183
184
export async function getServerSideProps(context: GetServerSidePropsContext) {
185
const { query } = context;
186
const id = extractID(query.id);
187
if (id == null) return NOT_FOUND;
188
189
try {
190
const { news, prev, next } = await getNewsItemUserPrevNext(id);
191
const { siteName, siteURL } = await getCustomize();
192
193
if (news == null) {
194
throw new Error(`not found`);
195
}
196
197
// Extract image URL from parsed Markdown. By converting to HTML first, we
198
// automatically add support for HTML that's been embedded into Markdown.
199
//
200
const $markdown = markdown_to_cheerio(news.text)
201
const imgSrc = $markdown('img')
202
.first()
203
.attr('src');
204
205
// Format published time
206
//
207
const publishedTime = formatNewsTime(news.date);
208
209
// Get the last-modified time by sorting the post history by timestamp,
210
// reversing it, and parsing the first element in that array.
211
//
212
const newsModificationTimestamps = Object.keys(news.history || {})
213
.map(Number)
214
.filter((ts) => !Number.isNaN(ts))
215
.sort()
216
.reverse();
217
218
const modifiedTime = newsModificationTimestamps.length
219
? formatNewsTime(newsModificationTimestamps[0])
220
: publishedTime;
221
222
const metadata: Props['metadata'] = {
223
title: news.title,
224
url: `${siteURL}${slugURL(news)}`,
225
image: imgSrc || '',
226
published: publishedTime,
227
modified: modifiedTime,
228
author: `${siteName}`,
229
};
230
231
return await withCustomize({
232
context,
233
props: {
234
news,
235
prev,
236
next,
237
metadata,
238
},
239
});
240
} catch (err) {
241
console.warn(`Error getting news with id=${id}`, err);
242
}
243
244
return NOT_FOUND;
245
}
246
247